Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new Ktor HTTP client Circuit Breaker plugin with per-name state machines (CLOSED → OPEN → HALF-OPEN), configurable thresholds, reset timeout, half-open trial limits, request tagging/routing, failure predicate, exception type, README, build files, and tests. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 173-176: The HALF_OPEN branch currently throws
CircuitBreakerOpenException when halfOpenAttempts >= config.halfOpenRequests
which misleadingly reports the circuit as OPEN; update this to either throw a
distinct exception (e.g., CircuitBreakerHalfOpenSaturatedException) or at
minimum change the error message to indicate the circuit is HALF_OPEN and
saturated (include name, config.resetTimeout and current
halfOpenAttempts/config.halfOpenRequests), and adjust any callers that rely on
CircuitBreakerOpenException to handle the new exception or message accordingly;
locate this logic in the block that checks CircuitState.HALF_OPEN and references
halfOpenAttempts and config.halfOpenRequests to make the change.
- Around line 351-355: The catch-all in CircuitBreaker.kt around the
proceed(request) call records every Throwable via instance.recordFailure(),
which incorrectly treats CancellationException as a service failure; update the
try/catch in the call execution (the block that calls proceed(request) inside
the CircuitBreaker plugin) to explicitly rethrow CancellationException (and
subclasses) without calling instance.recordFailure(), and only call
instance.recordFailure() for non-cancellation throwables; then add a regression
test exercising a locally-cancelled request to assert the circuit state does not
record a failure (i.e., circuit stays closed).
- Around line 329-335: The unsynchronized read of the MutableMap instances in
getInstance creates a race; move the initial lookup inside the
instancesMutex.withLock so the entire lookup-and-create path is protected.
Specifically, remove the pre-lock instances[name]?.let return check and perform
instances.getOrPut(name) { CircuitBreakerInstance(name, config.globalConfig,
timeSource) } inside instancesMutex.withLock in the getInstance function so all
access to instances is guarded by instancesMutex.
In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 13-30: The fenced diagram in the README.md lacks a language tag
which triggers markdownlint MD040; update the fence that contains the
circuit-breaker state diagram so the opening triple-backtick includes the
language identifier text (i.e., change the fence from ``` to ```text) so the
markdown linter recognizes it as a plain text block and the lint error is
resolved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4e5c17e8-4d22-40e4-b796-0e667c5fec4a
📒 Files selected for processing (7)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.mdktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.ktsktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.ktktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.ktsettings.gradle.kts
54fb87c to
552b182
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (4)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md (1)
13-30:⚠️ Potential issue | 🟡 MinorStill missing a language on the diagram fence.
Line 13 still opens the state-diagram block without a language, so markdownlint MD040 will keep firing.
✏️ Suggested fix
-``` +```text🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md` around lines 13 - 30, The code block containing the ASCII state diagram in README.md is missing a language specifier; update the opening fence (the triple backticks that start the ASCII diagram block) to include a language such as text (i.e., change ``` to ```text) so markdownlint MD040 stops flagging it and the diagram renders correctly.ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (3)
329-335:⚠️ Potential issue | 🔴 CriticalStill reading
instancesoutsideinstancesMutex.Line 330 performs an unsynchronized read from a shared
MutableMapbefore acquiringinstancesMutex. Since dynamic circuit creation writes to the same map inside the lock, this is still a real race; please move the whole lookup into the critical section.🔒 Suggested fix
suspend fun getInstance(name: String): CircuitBreakerInstance { - instances[name]?.let { return it } return instancesMutex.withLock { instances.getOrPut(name) { CircuitBreakerInstance(name, config.globalConfig, timeSource) } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 329 - 335, The current getInstance reads the shared MutableMap instances outside the instancesMutex, causing a race; move the entire lookup/creation logic into the critical section by removing the early unsynchronized instances[name] check and performing instances.getOrPut(name) inside instancesMutex.withLock so that all accesses to instances (read or write) happen under instancesMutex; keep creating the CircuitBreakerInstance(name, config.globalConfig, timeSource) as the getOrPut default.
351-355:⚠️ Potential issue | 🟠 MajorStill counting cancellations as downstream failures.
Line 353 catches every
Throwable, and Line 354 records it as a circuit failure. That still includes coroutine cancellation, so caller-aborted requests can open the breaker even when the service is healthy. Re-throwCancellationExceptionbeforerecordFailure(), add a regression test for a locally cancelled request, and tighten the README wording so it does not imply that all exceptions count.🛠️ Suggested fix
val call = try { proceed(request) } catch (cause: Throwable) { + if (cause is CancellationException) throw cause instance.recordFailure() throw cause }This checks the current catch block, confirms how
CancellationExceptionis defined in the repo, and shows whether this plugin already has cancellation-focused coverage. Expect the catch block to lack a cancellation guard and the test search to return no dedicated regression case.#!/bin/bash set -euo pipefail FILE='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt' TEST_FILE='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt' echo "== catch block in CircuitBreaker plugin ==" sed -n '349,356p' "$FILE" echo echo "== repo definitions of CancellationException ==" rg -n --type=kt 'typealias\s+CancellationException\s*=|class\s+CancellationException\b' echo echo "== cancellation-related coverage in CircuitBreakerTest ==" rg -n --type=kt 'CancellationException|cancel' "$TEST_FILE"Based on learnings,
io.ktor.utils.io.CancellationExceptionis a typealias forkotlinx.coroutines.CancellationException, so that single type check excludes all coroutine cancellation exceptions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 351 - 355, The catch in the CircuitBreaker around proceed(request) currently treats every Throwable as a failure; change the handler so that CancellationException is re-thrown immediately (e.g., in the catch for cause: Throwable do if (cause is CancellationException) throw cause) before calling instance.recordFailure() and re-throwing other exceptions; add a regression test in CircuitBreakerTest that cancels a locally started request and asserts the breaker does not record/open from that cancellation; and update the README wording for the circuit breaker to explicitly state that coroutine cancellations are excluded from failure counts.
173-176:⚠️ Potential issue | 🟡 MinorStill reporting HALF_OPEN saturation as
OPEN.Line 175 rejects extra half-open probes with
CircuitBreakerOpenException, but this branch is saturatedHALF_OPEN, notOPEN. That makes the exposed state and retry timing wrong for callers, because another request may be allowed as soon as a probe finishes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 173 - 176, The code currently throws CircuitBreakerOpenException when halfOpenAttempts >= config.halfOpenRequests inside the CircuitState.HALF_OPEN branch; change this to surface the correct HALF_OPEN condition: replace the thrown CircuitBreakerOpenException(name, config.resetTimeout) with a distinct exception (e.g., CircuitBreakerHalfOpenException) that indicates the circuit is saturated in HALF_OPEN (or extend CircuitBreakerOpenException to accept a state parameter and pass CircuitState.HALF_OPEN). Update any callers/tests to expect the new exception type or check the returned state accordingly; reference CircuitState.HALF_OPEN, halfOpenAttempts, config.halfOpenRequests, and the thrown exception in your changes.
🧹 Nitpick comments (1)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (1)
267-277: Add the standard feedback link to this public extension KDoc.
HttpRequestBuilder.circuitBreakeris public API, but its KDoc stops after the description. Please add the repo’s usual[Report a problem]footer so the generated docs stay consistent with other public extensions. Based on learnings, public API extension functions in this repo should include a brief description and a[Report a problem]link.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 267 - 277, The public extension function HttpRequestBuilder.circuitBreaker is missing the repo's standard "[Report a problem]" KDoc footer; update the KDoc for circuitBreaker (the block above the function that references CircuitBreakerConfig.register and CircuitBreakerConfig.global) to append the standard Report a problem link/footer used across public APIs so generated docs match other extensions, keeping existing description and references to CircuitBreakerNameKey unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 329-335: The current getInstance reads the shared MutableMap
instances outside the instancesMutex, causing a race; move the entire
lookup/creation logic into the critical section by removing the early
unsynchronized instances[name] check and performing instances.getOrPut(name)
inside instancesMutex.withLock so that all accesses to instances (read or write)
happen under instancesMutex; keep creating the CircuitBreakerInstance(name,
config.globalConfig, timeSource) as the getOrPut default.
- Around line 351-355: The catch in the CircuitBreaker around proceed(request)
currently treats every Throwable as a failure; change the handler so that
CancellationException is re-thrown immediately (e.g., in the catch for cause:
Throwable do if (cause is CancellationException) throw cause) before calling
instance.recordFailure() and re-throwing other exceptions; add a regression test
in CircuitBreakerTest that cancels a locally started request and asserts the
breaker does not record/open from that cancellation; and update the README
wording for the circuit breaker to explicitly state that coroutine cancellations
are excluded from failure counts.
- Around line 173-176: The code currently throws CircuitBreakerOpenException
when halfOpenAttempts >= config.halfOpenRequests inside the
CircuitState.HALF_OPEN branch; change this to surface the correct HALF_OPEN
condition: replace the thrown CircuitBreakerOpenException(name,
config.resetTimeout) with a distinct exception (e.g.,
CircuitBreakerHalfOpenException) that indicates the circuit is saturated in
HALF_OPEN (or extend CircuitBreakerOpenException to accept a state parameter and
pass CircuitState.HALF_OPEN). Update any callers/tests to expect the new
exception type or check the returned state accordingly; reference
CircuitState.HALF_OPEN, halfOpenAttempts, config.halfOpenRequests, and the
thrown exception in your changes.
In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 13-30: The code block containing the ASCII state diagram in
README.md is missing a language specifier; update the opening fence (the triple
backticks that start the ASCII diagram block) to include a language such as text
(i.e., change ``` to ```text) so markdownlint MD040 stops flagging it and the
diagram renders correctly.
---
Nitpick comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 267-277: The public extension function
HttpRequestBuilder.circuitBreaker is missing the repo's standard "[Report a
problem]" KDoc footer; update the KDoc for circuitBreaker (the block above the
function that references CircuitBreakerConfig.register and
CircuitBreakerConfig.global) to append the standard Report a problem link/footer
used across public APIs so generated docs match other extensions, keeping
existing description and references to CircuitBreakerNameKey unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 19839744-739a-4dc8-bcd2-688a4f66c336
📒 Files selected for processing (7)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.mdktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.ktsktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.ktktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.ktsettings.gradle.kts
✅ Files skipped from review due to trivial changes (3)
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
🚧 Files skipped from review as they are similar to previous changes (2)
- settings.gradle.kts
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
552b182 to
71282ee
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 145-146: The README's plugin order is reversed: change the
guidance so that CircuitBreaker is installed before HttpRequestRetry (i.e.,
install `CircuitBreaker` before `HttpRequestRetry`) so the circuit breaker wraps
the retry logic and observes the final outcome after all retries; keep the
`HttpTimeout` note as-is (timeout exceptions count as circuit breaker failures).
Ensure the text references the exact symbols `CircuitBreaker`,
`HttpRequestRetry`, and `HttpTimeout`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c2c1d084-3e78-4af9-b061-4d26765b42ce
📒 Files selected for processing (7)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.mdktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.apiktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.ktsktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.ktktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.ktsettings.gradle.kts
✅ Files skipped from review due to trivial changes (3)
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
- settings.gradle.kts
- ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
…mmon/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ADME.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md (1)
13-13:⚠️ Potential issue | 🟡 MinorAdd
textto the ASCII-diagram fence.The opening fence at Line 13 still trips markdownlint MD040 without a language tag.
✏️ Proposed fix
-``` +```text🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md` at line 13, The markdown code fence in the ASCII diagram uses a bare triple-backtick opening fence which triggers markdownlint MD040; update the opening fence to include a language tag by replacing the opening "```" with "```text" so the ASCII-diagram block has a language specified (e.g., change the fence at the ASCII-diagram opening to "```text").ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (2)
173-176:⚠️ Potential issue | 🟡 MinorUse a HALF_OPEN-specific rejection here.
Line 175 is rejecting because the half-open probe budget is full, not because the circuit is OPEN until
resetTimeoutelapses. ReusingCircuitBreakerOpenExceptionmakes the public exception, message, and KDoc inaccurate for callers. A distinct rejection path, or at least a HALF_OPEN-specific message/KDoc, would avoid that.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 173 - 176, The current HALF_OPEN branch throws CircuitBreakerOpenException which misrepresents why a request is rejected; change this to a distinct HALF_OPEN-specific rejection (e.g., introduce and throw CircuitBreakerHalfOpenException or similar) so the public exception, message and KDoc reflect that the half-open probe budget (halfOpenAttempts >= config.halfOpenRequests) is exhausted rather than the circuit being open until config.resetTimeout; update the thrown type, its message to mention half-open probe limit and adjust any KDoc/comments referencing CircuitState.HALF_OPEN and resetTimeout accordingly.
351-355:⚠️ Potential issue | 🟠 MajorDon’t count coroutine cancellations as downstream failures.
catch (Throwable)currently callsrecordFailure()for caller/scope cancellations too, so a locally aborted request can open the circuit even when the service is healthy. Re-throwCancellationExceptionbefore the catch-all and add a regression test for it; the README note that exceptions “always” count as failures will need the same carve-out.🛠️ Proposed fix
val call = try { proceed(request) + } catch (cause: CancellationException) { + throw cause } catch (cause: Throwable) { instance.recordFailure() throw cause }🔎 Verification script
#!/bin/bash file='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt' test_file='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt' sed -n '344,356p' "$file" printf '\n-- cancellation-related matches --\n' rg -n -C2 'CancellationException|catch \(cause: Throwable\)|recordFailure\(|cancel' "$file" "$test_file"Based on learnings, "In Ktor,
io.ktor.utils.io.CancellationExceptionis a typealias forkotlinx.coroutines.CancellationException, which is a supertype ofJobCancellationException. Therefore, checkinge !is io.ktor.utils.io.CancellationExceptionis sufficient to exclude all coroutine cancellation exceptions."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt` around lines 351 - 355, The catch-all currently treats every Throwable as a downstream failure by calling instance.recordFailure() inside the catch (cause: Throwable) block; change the control flow so CancellationException (use io.ktor.utils.io.CancellationException / kotlinx.coroutines.CancellationException) is re-thrown before the generic catch so cancellations do not call instance.recordFailure(), i.e. add an explicit check for CancellationException and rethrow it (or add a specific catch for it) and only call instance.recordFailure() for non-cancellation throwables; update the tests by adding a regression test in CircuitBreakerTest that simulates a caller/scope cancellation and asserts the circuit is not opened, and adjust the README note about exceptions counting as failures to exclude coroutine cancellations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 329-336: There is a stray closing brace that prematurely ends the
createClientPlugin builder; remove the extra `}` after the getInstance function
so the builder block continues and ensure that resolveCircuitName and the
on(Send) interceptor remain defined inside the createClientPlugin scope (look
for createClientPlugin { ... }, getInstance(...) and then the resolveCircuitName
and on(Send) declarations) so the interceptor code is compiled as part of the
plugin.
---
Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 173-176: The current HALF_OPEN branch throws
CircuitBreakerOpenException which misrepresents why a request is rejected;
change this to a distinct HALF_OPEN-specific rejection (e.g., introduce and
throw CircuitBreakerHalfOpenException or similar) so the public exception,
message and KDoc reflect that the half-open probe budget (halfOpenAttempts >=
config.halfOpenRequests) is exhausted rather than the circuit being open until
config.resetTimeout; update the thrown type, its message to mention half-open
probe limit and adjust any KDoc/comments referencing CircuitState.HALF_OPEN and
resetTimeout accordingly.
- Around line 351-355: The catch-all currently treats every Throwable as a
downstream failure by calling instance.recordFailure() inside the catch (cause:
Throwable) block; change the control flow so CancellationException (use
io.ktor.utils.io.CancellationException /
kotlinx.coroutines.CancellationException) is re-thrown before the generic catch
so cancellations do not call instance.recordFailure(), i.e. add an explicit
check for CancellationException and rethrow it (or add a specific catch for it)
and only call instance.recordFailure() for non-cancellation throwables; update
the tests by adding a regression test in CircuitBreakerTest that simulates a
caller/scope cancellation and asserts the circuit is not opened, and adjust the
README note about exceptions counting as failures to exclude coroutine
cancellations.
In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Line 13: The markdown code fence in the ASCII diagram uses a bare
triple-backtick opening fence which triggers markdownlint MD040; update the
opening fence to include a language tag by replacing the opening "```" with
"```text" so the ASCII-diagram block has a language specified (e.g., change the
fence at the ASCII-diagram opening to "```text").
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 65255ade-2e7c-4167-bfe9-c0114371a5b7
📒 Files selected for processing (2)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.mdktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt
…mmon/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ted half-open states
|
Thanks for your work on this 🙏 |
PR: Add Circuit Breaker client plugin
Summary
ktor-client-circuit-breakermodule implementing the Circuit Breaker pattern for the Ktor HTTP client, filling the gap alongsideHttpRequestRetryandHttpTimeoutfor microservice resilience.Mutex.Motivation
The Ktor client has retry and timeout support but no built-in circuit breaker. Without one, a single failing downstream service can exhaust connection pools and cascade failures across the call graph. This is a table-stakes pattern for microservice architectures and is present in competing frameworks (Spring Cloud Circuit Breaker, Resilience4j, Hystrix).
API
Design decisions
on(Send)hook -- Same interception point asHttpRequestRetry, giving the circuit breaker visibility into the final outcome after any inner retry logic.IllegalStateException--CircuitBreakerOpenExceptionextendsIllegalStateExceptionrather thanIOExceptionbecause a circuit breaker rejection is a deliberate policy decision, not an I/O error. This also avoids unintended retries byHttpRequestRetry's default exception predicate.TimeSourceabstraction -- An internaltimeSourceproperty defaults toTimeSource.Monotonicbut can be replaced withTestTimeSourcein tests, enabling deterministic time-based state transition tests without real delays.Mutex-- Used for state transitions instead of atomics, since the state machine has multi-field invariants that need to be updated atomically.Files changed
settings.gradle.ktsktor-client-circuit-breakermodule.../ktor-client-circuit-breaker/build.gradle.kts.../circuitbreaker/CircuitBreaker.kt.../circuitbreaker/CircuitBreakerTest.kt.../api/ktor-client-circuit-breaker.api.../api/ktor-client-circuit-breaker.klib.api.../ktor-client-circuit-breaker/README.md